Link to this headingDNS

https://jan.wildeboer.net/2025/08/My-DNS-Part-1/ deploying your own DNS

https://technitium.com/dns/ new DNS server

Link to this headingDNSSEC

Can only check if a DNS record has been changed.

Link to this headingAlgorithms

NumberMnemonicsDNSSEC SigningDNSSEC Validation
1RSAMD5MUST NOTMUST NOT
3DSAMUST NOTMUST NOT
5RSASHA1NOT RECOMMENDEDMUST
6DSA-NSEC3-SHA1MUST NOTMUST NOT
7RSASHA1-NSEC3-SHA1NOT RECOMMENDEDMUST
8RSASHA256MUSTMUST
10RSASHA512NOT RECOMMENDEDMUST
12ECC-GOSTMUST NOTMAY
13ECDSAP256SHA256MUSTMUST
14ECDSAP384SHA384MAYRECOMMENDED
15ED25519RECOMMENDEDRECOMMENDED
16ED448MAYRECOMMENDED

Link to this headingKeys

Get Top Level DNS Key:

>>> dig @ganz.ns.cloudflare.com. generalzero.org DNSKEY +dnssec ; <<>> DiG 9.20.4 <<>> @ganz.ns.cloudflare.com. generalzero.org DNSKEY +dnssec ; (6 servers found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 57095 ;; flags: qr aa rd; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1 ;; WARNING: recursion requested but not available ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags: do; udp: 1232 ;; QUESTION SECTION: ;generalzero.org. IN DNSKEY ;; ANSWER SECTION: generalzero.org. 3600 IN DNSKEY 257 3 13 mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+ KkxLbxILfDLUT0rAK9iUzy1L53eKGQ== generalzero.org. 3600 IN DNSKEY 256 3 13 oJMRESz5E4gYzS/q6XDrvU1qMPYIjCWzJaOau8XNEZeqCYKD5ar0IRd8 KqXXFJkqmVfRvMGPmM1x8fGAa2XhSA== generalzero.org. 3600 IN RRSIG DNSKEY 13 2 3600 20250305013940 20250103013940 2371 generalzero.org. reY4OL9yVqdZQYpbG6+n+Kb7kD5wpYPy4nxznuIErhp9uqZ8IpM+8YbG OY8dkk89dZlPBnQjC8+uAqHmxK6pHA== ;; Query time: 3 msec ;; SERVER: 2606:4700:58::a29f:2c28#53(ganz.ns.cloudflare.com.) (UDP) ;; WHEN: Tue Jan 07 22:21:00 EST 2025 ;; MSG SIZE rcvd: 315

The DNSKEY Response contains 2 DNSKEY Keys. These are differentiated by the flags.
Key Signing Key (Flag 257): The key that is used to sign the Zone Signing Key with the signature information in the RRSIG info.
Zone Signing Key (Flag 256): The key that is used to sign DNS records and subdomains with RRSIG data.

DNSSec Record:

>>> dig www.generalzero.org +dnssec ; <<>> DiG 9.20.4 <<>> www.generalzero.org +dnssec ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 59755 ;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags: do; udp: 65494 ;; QUESTION SECTION: ;www.generalzero.org. IN A ;; ANSWER SECTION: www.generalzero.org. 253 IN A 172.67.179.53 www.generalzero.org. 253 IN A 104.21.91.200 www.generalzero.org. 253 IN RRSIG A 13 3 300 20250107222441 20250105202441 34505 generalzero.org. bCLHO171apwxiIvCI0zZYgHWBX6CmtpdvsKDykystJM+2IEXmQPsocv7 SUYyErUAKLf7VwKaSufHy+fdgMkO1Q== ;; Query time: 0 msec ;; SERVER: 127.0.0.53#53(127.0.0.53) (UDP) ;; WHEN: Mon Jan 06 16:25:27 EST 2025 ;; MSG SIZE rcvd: 191

Link to this headingImplementation

DNSSec Manual Verify:

import dns.dnssec import dns.resolver import dns.query import dns.message import base64, sys from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives import hashes, serialization from cryptography.exceptions import InvalidSignature from dns.dnssecalgs import ( # pylint: disable=C0412 get_algorithm_cls_from_dnskey, ) def parse_dnskey_to_public_key(dnskey): """Convert a DNSKEY record to a public key object, supporting various algorithms.""" #print(f"DNSKey: {dnskey.flags} {dnskey.protocol} {dnskey.algorithm} {dnskey.key}") #flags, protocol, algorithm, key = [dnskey.flags, dnskey.protocol, dnskey.algorithm, dnskey.key] return get_algorithm_cls_from_dnskey(dnskey).public_cls.from_dnskey(dnskey) def resolve_ipv4(hostname): """Resolve a hostname to an IP address.""" answer = dns.resolver.resolve(hostname, 'A') # Query for IPv4 return answer[0].to_text() def validate_domain_dnskey(domain, ns_address="1.1.1.1", timeout=20): # Get DNSKey from Name Servers request = dns.message.make_query(domain, dns.rdatatype.DNSKEY, want_dnssec=True) try: response = dns.query.udp(request, ns_address, timeout=timeout) # Check if valid response if response.rcode() != 0: print("❌ ERROR: no DNSKEY record found or SERVEFAIL") return # Check if key exists answer = response.answer if len(answer) != 2: print("❌ ERROR: could not find RRSET record (DNSKEY and RR DNSKEY) in zone") return # Check if the DNSKEY record is signed, RRSET validation name = dns.name.from_text(domain) # Get DNSKeys signing_keys = [] signing_key = None # Get keys from response for rrset in answer: if rrset.rdtype == dns.rdatatype.DNSKEY: dnskeys = rrset elif rrset.rdtype == dns.rdatatype.RRSIG: rrsigs = rrset for dnskey in dnskeys: if dnskey.flags == 256: # Zone Signing Key zone_key = parse_dnskey_to_public_key(dnskey) elif dnskey.flags == 257: # Key Signing Key signing_keys.append(parse_dnskey_to_public_key(dnskey)) # Validate RRSIG for rrsig in rrsigs: print(f"Signature: {rrsig.signature.hex()}") dns.dnssec._validate_rrsig(dnskeys, rrsig, {name: dnskeys}) data = dns.dnssec._make_rrsig_signature_data(dnskeys, rrsig) #print(f"SignData: {data.hex()}") #print(f"Public Key: {signing_key.key.public_bytes(encoding=serialization.Encoding.PEM,format=serialization.PublicFormat.SubjectPublicKeyInfo)}") try: #Get the correct signing key # Get the correct signing key if signing_key.key_tag == rrsig.key_tag: signing_key.verify(rrsig.signature, data) print(f"✅ DNSKEY RRSIG is valid for {domain} using the Key Signing Key") break else: print(f"❌ No Signing Key with keytag={rrsig.key_tag}") except InvalidSignature as e: print(f"❌ DNSKEY RRSIG is invalid for {domain} using the Key Signing Key") except Exception as e: print(e) except Exception as e: print(e) return zone_key, signing_key def validate_dnssec_resp(domain, record_type, ns_address="1.1.1.1", timeout=20): """Validate DNSSEC for the given domain and record type.""" # Fetch the specified record_type from the nameserver try: request = dns.message.make_query(domain, record_type, want_dnssec=True) response = dns.query.udp(request, ns_address, timeout=timeout) # Get the answer for rrset in response.answer: if rrset.rdtype == dns.rdatatype.RRSIG: dns_rrsigs = rrset else: dns_answer = rrset #Get and Verify DNSKEYs # Get and verify DNSKEYs # Validate record_type RRSIG with the zone_signing_key for rrsig in dns_rrsigs: data = dns.dnssec._make_rrsig_signature_data(dns_answer, rrsig) #print(f"Signature: {rrsig.signature.hex()}") #print(f"SignData: {data.hex()}") #print(f"Public Key: {zone_signing_key.key.public_bytes(encoding=serialization.Encoding.PEM,format=serialization.PublicFormat.SubjectPublicKeyInfo)}") try: zone_signing_key.verify(rrsig.signature, data) print(f"✅ RRSIG for {record_type} using {domain} zone_signing_key has been verified") return dns_answer except InvalidSignature: print(f"❌ RRSIG for {record_type} using {domain} zone_signing_key is invalid") return except Exception as e: print(f"Error validating DNSSEC for {domain}: {e}") def validate_dnssec_ds_resp(domain, parent_domain, ns_address="1.1.1.1", timeout=20): """Validate DNSSEC for the given domain and record type.""" # Fetch the specified record_type from the nameserver try: request = dns.message.make_query(domain, "DS", want_dnssec=True) response = dns.query.udp(request, ns_address, timeout=timeout) # Get the answer for rrset in response.answer: if rrset.rdtype == dns.rdatatype.RRSIG: dns_rrsigs = rrset else: dns_answer = rrset # Get and verify DNSKEYs zone_signing_key, key_signing_key = validate_domain_dnskey(parent_domain) # Validate record_type RRSIG with the zone_signing_key for rrsig in dns_rrsigs: data = dns.dnssec._make_rrsig_signature_data(dns_answer, rrsig) #print(f"Signature: {rrsig.signature.hex()}") #print(f"SignData: {data.hex()}") #print(f"Public Key: {zone_signing_key.key.public_bytes(encoding=serialization.Encoding.PEM,format=serialization.PublicFormat.SubjectPublicKeyInfo)}") try: zone_signing_key.verify(rrsig.signature, data) print(f"✅ RRSIG for DS using {domain} zone_signing_key has been verified") return dns_answer except InvalidSignature: print(f"❌ RRSIG for DS using {domain} zone_signing_key is invalid") return except Exception as e: print(f"❌ Error validating DNSSEC for {domain}: {e}") # Update __main__ to include subdomain validation if __name__ == "__main__": domain = "example.com." top_level_domain = "com." root_domain = "." record_type = "A" ns_server = None #Do a DNSSEC A Record Query for example.com # Do a DNSSEC A Record Query for example.com # Get Response for Query with RRsig # Get DNSKeys for example.com # Verify DNSKEY RRsig for example.com domain # Verify A RRSig with example.com zone_signing_key DNSKey #Do a DNSSEC DS Record Query for example.com # Do a DNSSEC DS Record Query for example.com # Get Response for Query with RRsig # Get DNSKeys for .com # Verify DNSKEY RRsig for .com domain # Verify DS RRSig with .com zone_signing_key DNSKey print(dns_answer) #Do a DNSSEC DS Record Query for .com # Do a DNSSEC DS Record Query for .com # Get Response for Query with RRsig # Get DNSKeys for . # Verify DNSKEY RRsig for . domain # Verify DS RRSig with . zone_signing_key DNSKey